From f351a7ae8e23ae45805765235b799c18435dfca4 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Tue, 17 Mar 2026 21:00:34 +0000 Subject: [PATCH] ci: Revamp bootc integration test suite with bcvk VM support the old privtest CI job with a new tests/bootc-integration Rust crate that runs inside a bcvk VM. The old tests/inst crate is preserved for tests not yet ported. Tests are split into two tiers based on what they need: - booted_test!: needs a fully deployed ostree system. Dispatches via `bcvk libvirt run` which does `bootc install to-disk`. - privileged_test!: just needs root. Dispatches via the faster `bcvk ephemeral run-ssh`. The Justfile provides `integration-container` (full suite) and `integration-ephemeral` (fast privileged-only path). JUnit XML output is supported via the JUNIT_OUTPUT env var using quick-junit; integration-container captures results to target/integration-results.xml. Drop the vendored bootc-ubuntu-setup action in favor of the upstream bootc-dev/actions/bootc-ubuntu-setup@main. Assisted-by: OpenCode (Claude claude-opus-4-6) Signed-off-by: Colin Walters --- .github/actions/bootc-ubuntu-setup/action.yml | 91 -- .github/workflows/bootc.yaml | 22 +- .github/workflows/tests.yml | 43 - Dockerfile | 17 + Justfile | 28 + tests/bootc-integration/.gitignore | 1 + tests/bootc-integration/Cargo.lock | 800 ++++++++++++++++++ tests/bootc-integration/Cargo.toml | 22 + tests/bootc-integration/bootc-e2e.md | 25 + tests/bootc-integration/src/lib.rs | 52 ++ tests/bootc-integration/src/main.rs | 93 ++ tests/bootc-integration/src/tests/mod.rs | 3 + .../bootc-integration/src/tests/privileged.rs | 338 ++++++++ 13 files changed, 1388 insertions(+), 147 deletions(-) delete mode 100644 .github/actions/bootc-ubuntu-setup/action.yml create mode 100644 tests/bootc-integration/.gitignore create mode 100644 tests/bootc-integration/Cargo.lock create mode 100644 tests/bootc-integration/Cargo.toml create mode 100644 tests/bootc-integration/bootc-e2e.md create mode 100644 tests/bootc-integration/src/lib.rs create mode 100644 tests/bootc-integration/src/main.rs create mode 100644 tests/bootc-integration/src/tests/mod.rs create mode 100644 tests/bootc-integration/src/tests/privileged.rs diff --git a/.github/actions/bootc-ubuntu-setup/action.yml b/.github/actions/bootc-ubuntu-setup/action.yml deleted file mode 100644 index 5bfcbb2e..00000000 --- a/.github/actions/bootc-ubuntu-setup/action.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: 'Bootc Ubuntu Setup' -description: 'Default host setup' -inputs: - libvirt: - description: 'Install libvirt and virtualization stack' - required: false - default: 'false' -runs: - using: 'composite' - steps: - # The default runners have TONS of crud on them... - - name: Free up disk space on runner - shell: bash - run: | - set -xeuo pipefail - sudo df -h - unwanted_pkgs=('^aspnetcore-.*' '^dotnet-.*' '^llvm-.*' 'php.*' '^mongodb-.*' '^mysql-.*' - azure-cli google-chrome-stable firefox mono-devel) - unwanted_dirs=(/usr/share/dotnet /opt/ghc /usr/local/lib/android /opt/hostedtoolcache/CodeQL) - # Start background removal operations as systemd units; if this causes - # races in the future around disk space we can look at waiting for cleanup - # before starting further jobs, but right now we spent a lot of time waiting - # on the network and scripts and such below, giving these plenty of time to run. - n=0 - runcleanup() { - sudo systemd-run -r -u action-cleanup-${n} -- "$@" - n=$(($n + 1)) - } - runcleanup docker image prune --all --force - for x in ${unwanted_dirs[@]}; do - runcleanup rm -rf "$x" - done - # Apt removals in foreground, as we can't parallelize these - for x in ${unwanted_pkgs[@]}; do - /bin/time -f '%E %C' sudo apt-get remove -y $x - done - # We really want support for heredocs - - name: Update podman and install just - shell: bash - run: | - set -eux - # Require the runner is ubuntu-24.04 - IDV=$(. /usr/lib/os-release && echo ${ID}-${VERSION_ID}) - test "${IDV}" = "ubuntu-24.04" - # plucky is the next release - echo 'deb http://azure.archive.ubuntu.com/ubuntu plucky universe main' | sudo tee /etc/apt/sources.list.d/plucky.list - /bin/time -f '%E %C' sudo apt update - # skopeo is currently older in plucky for some reason hence --allow-downgrades - /bin/time -f '%E %C' sudo apt install -y --allow-downgrades crun/plucky podman/plucky skopeo/plucky just - # This is the default on e.g. Fedora derivatives, but not Debian - - name: Enable unprivileged /dev/kvm access - shell: bash - run: | - set -xeuo pipefail - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - ls -l /dev/kvm - # Used by a few workflows, but generally useful - - name: Set architecture variable - id: set_arch - shell: bash - run: echo "ARCH=$(arch)" >> $GITHUB_ENV - # Install libvirt stack if requested - - name: Install libvirt and virtualization stack - if: ${{ inputs.libvirt == 'true' }} - shell: bash - run: | - set -xeuo pipefail - export BCVK_VERSION=0.8.0 - /bin/time -f '%E %C' sudo apt install -y libkrb5-dev pkg-config libvirt-dev genisoimage qemu-utils qemu-kvm virtiofsd libvirt-daemon-system - # Something in the stack is overriding this, but we want session right now for bcvk - echo LIBVIRT_DEFAULT_URI=qemu:///session >> $GITHUB_ENV - td=$(mktemp -d) - cd $td - # Install bcvk - target=bcvk-$(arch)-unknown-linux-gnu - /bin/time -f '%E %C' curl -LO https://github.com/bootc-dev/bcvk/releases/download/v${BCVK_VERSION}/${target}.tar.gz - tar xzf ${target}.tar.gz - sudo install -T ${target} /usr/bin/bcvk - cd - - rm -rf "$td" - - # Also bump the default fd limit as a workaround for https://github.com/bootc-dev/bcvk/issues/65 - sudo sed -i -e 's,^\* hard nofile 65536,* hard nofile 524288,' /etc/security/limits.conf - - name: Cleanup status - shell: bash - run: | - set -xeuo pipefail - systemctl list-units 'action-cleanup*' - df -h diff --git a/.github/workflows/bootc.yaml b/.github/workflows/bootc.yaml index b3d6ef24..d7142110 100644 --- a/.github/workflows/bootc.yaml +++ b/.github/workflows/bootc.yaml @@ -23,22 +23,18 @@ jobs: stream: [stream9, stream10] steps: - uses: actions/checkout@v4 - - uses: ./.github/actions/bootc-ubuntu-setup + - uses: bootc-dev/actions/bootc-ubuntu-setup@main + with: + libvirt: true - name: build - run: sudo STREAM=${{ matrix.stream }} just build + run: STREAM=${{ matrix.stream }} just build - name: unitcontainer - run: sudo STREAM=${{ matrix.stream }} just unitcontainer + run: STREAM=${{ matrix.stream }} just unitcontainer - name: unittest - run: sudo STREAM=${{ matrix.stream }} just unittest - - name: bootc install - run: | - set -xeuo pipefail - sudo podman run --env BOOTC_SKIP_SELINUX_HOST_CHECK=1 --rm -ti --privileged -v /:/target --pid=host --security-opt label=disable \ - -v /dev:/dev -v /var/lib/containers:/var/lib/containers \ - localhost/ostree:latest bootc install to-filesystem --skip-fetch-check \ - --replace=alongside /target - # Verify labeling for /etc - sudo ls -dZ /ostree/deploy/default/deploy/*.0/etc |grep :etc_t: + run: STREAM=${{ matrix.stream }} just unittest + - name: integration tests (bcvk) + timeout-minutes: 15 + run: STREAM=${{ matrix.stream }} just integration-container - name: Upload test logs if: failure() uses: actions/upload-artifact@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5364429e..af83c014 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -41,22 +41,6 @@ jobs: - run: sudo apt install clang-format just - name: Test style run: just clang-format-check - build-integration: - runs-on: ubuntu-latest - container: quay.io/coreos-assembler/fcos-buildroot:testing-devel - steps: - - uses: actions/checkout@v3 - - name: Cache Dependencies - uses: Swatinem/rust-cache@ce325b60658c1b38465c06cc965b79baf32c1e72 - with: - key: "integration" - - name: Build - run: cd tests/inst && cargo build --verbose --release - - name: Upload binary - uses: actions/upload-artifact@v4 - with: - name: ostree-test - path: tests/inst/target/release/ostree-test minimal: name: "Build - FCOS minimal" runs-on: ubuntu-latest @@ -95,33 +79,6 @@ jobs: with: name: inst.tar.zst path: inst.tar.zst - privtest: - name: "Privileged testing" - needs: [build-c, build-integration] - runs-on: ubuntu-latest - container: - image: quay.io/fedora/fedora-coreos:testing-devel - options: "--privileged --pid=host -v /run/systemd:/run/systemd -v /:/run/host" - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - name: Download install tree - uses: actions/download-artifact@v4 - with: - name: inst.tar.zst - - name: Install - run: tar -C / -xvf inst.tar.zst && rm -f inst.tar.zst - - name: Download test binary - uses: actions/download-artifact@v4 - with: - name: ostree-test - - name: Install - run: install ostree-test /usr/bin - - name: Setup - # https://github.com/ostreedev/ostree-rs-ext/issues/417 - run: mkdir -p /var/tmp - - name: Integration tests (unit) - run: ostree-test tests: # Distro configuration matrix # diff --git a/Dockerfile b/Dockerfile index 412f21a4..3dd64c24 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,6 +39,22 @@ RUN rpm -e --nodeps ostree{,-libs} COPY --from=build /out/ / COPY --from=src /src/tests-unit-container /tests +# Build integration test binary +FROM $base as integration-build +RUN dnf -y install cargo rust +COPY tests/bootc-integration/Cargo.toml tests/bootc-integration/Cargo.lock /build/tests/bootc-integration/ +WORKDIR /build/tests/bootc-integration +RUN --mount=type=cache,target=/root/.cargo/registry \ + --mount=type=cache,target=/root/.cargo/git \ + cargo fetch +COPY tests/bootc-integration /build/tests/bootc-integration +RUN --network=none \ + --mount=type=cache,target=/root/.cargo/registry \ + --mount=type=cache,target=/root/.cargo/git \ + --mount=type=cache,target=/build/tests/bootc-integration/target \ + cargo build --release && \ + cp target/release/ostree-bootc-integration-tests /usr/bin/ostree-bootc-integration-tests + # Override userspace FROM $base as rootfs # Remove the default binaries to ensure we're getting our overrides @@ -48,6 +64,7 @@ COPY --from=build /out/ / # The default final container, with also a regenerated # initramfs in case ostree-prepare-root changed. FROM rootfs +COPY --from=integration-build /usr/bin/ostree-bootc-integration-tests /usr/bin/ostree-bootc-integration-tests # https://docs.fedoraproject.org/en-US/bootc/initramfs/#_regenerating_the_initrd # since we have ostree-prepare-root there RUN set -x; kver=$(cd /usr/lib/modules && echo *); dracut -vf /usr/lib/modules/$kver/initramfs.img $kver diff --git a/Justfile b/Justfile index c1316990..b2f53c14 100644 --- a/Justfile +++ b/Justfile @@ -56,6 +56,34 @@ build-host-inst: build-host make -C target/c install DESTDIR=$(pwd)/target/inst tar --sort=name --numeric-owner --owner=0 --group=0 -C target/inst -czf target/inst.tar.gz . +# Run all integration tests inside a deployed libvirt VM. +# This runs both "booted" tests (need full deployment) and "privileged" +# tests (just need root). For faster iteration on privileged-only tests, +# use `integration-ephemeral`. +# JUnit XML results are written to target/integration-results.xml. +integration-container *ARGS: + #!/bin/bash + set -euo pipefail + bcvk libvirt run --name ostree-integration-test --replace --detach --ssh-wait localhost/ostree:latest + rc=0 + bcvk libvirt ssh ostree-integration-test -- env JUNIT_OUTPUT=/tmp/junit.xml \ + ostree-bootc-integration-tests {{ARGS}} || rc=$? + mkdir -p target + bcvk libvirt ssh ostree-integration-test -- cat /tmp/junit.xml \ + > target/integration-results.xml 2>/dev/null || true + bcvk libvirt rm --stop --force ostree-integration-test + exit $rc + +# Run only root-privileged tests via an ephemeral VM (faster — no disk install). +# This skips tests that need a fully deployed/booted ostree system. +# Not all tests will pass here; use `integration-container` for the full suite. +integration-ephemeral *ARGS: + bcvk ephemeral run-ssh localhost/ostree:latest -- ostree-bootc-integration-tests {{ARGS}} + +# Remove any leftover integration test VMs +integration-cleanup: + -bcvk libvirt rm --stop --force ostree-integration-test + sourcefiles := "git ls-files '**.c' '**.cxx' '**.h' '**.hpp'" # Reformat source files clang-format: diff --git a/tests/bootc-integration/.gitignore b/tests/bootc-integration/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/tests/bootc-integration/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/tests/bootc-integration/Cargo.lock b/tests/bootc-integration/Cargo.lock new file mode 100644 index 00000000..3dcd2048 --- /dev/null +++ b/tests/bootc-integration/Cargo.lock @@ -0,0 +1,800 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "num-traits", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "escape8259" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5692dd7b5a1978a5aeb0ce83b7655c58ca8efdcb79d21036ea249da95afec2c6" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libtest-mimic" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5297962ef19edda4ce33aaa484386e0a5b3d7f2f4e037cbeee00503ef6b29d33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + +[[package]] +name = "linkme" +version = "0.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e3283ed2d0e50c06dd8602e0ab319bb048b6325d0bba739db64ed8205179898" +dependencies = [ + "linkme-impl", +] + +[[package]] +name = "linkme-impl" +version = "0.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5cec0ec4228b4853bb129c84dbf093a27e6c7a20526da046defc334a1b017f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "newtype-uuid" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c012d14ef788ab066a347d19e3dda699916c92293b05b85ba2c76b8c82d2830" +dependencies = [ + "uuid", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "ostree-bootc-integration-tests" +version = "0.1.0" +dependencies = [ + "anyhow", + "libtest-mimic", + "linkme", + "paste", + "quick-junit", + "rustix", + "tempfile", + "xshell", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-junit" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ee9342d671fae8d66b3ae9fd7a9714dfd089c04d2a8b1ec0436ef77aee15e5f" +dependencies = [ + "chrono", + "indexmap", + "newtype-uuid", + "quick-xml", + "strip-ansi-escapes", + "thiserror", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "xshell" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7290c623014758632efe00737145b6867b66292c42167f2ec381eb566a373d" +dependencies = [ + "xshell-macros", +] + +[[package]] +name = "xshell-macros" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/tests/bootc-integration/Cargo.toml b/tests/bootc-integration/Cargo.toml new file mode 100644 index 00000000..af3b21e1 --- /dev/null +++ b/tests/bootc-integration/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ostree-bootc-integration-tests" +version = "0.1.0" +edition = "2021" +publish = false + +# Separate workspace from root; this is built inside the container +[workspace] + +[[bin]] +name = "ostree-bootc-integration-tests" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +libtest-mimic = "0.8" +linkme = "0.3" +paste = "1" +quick-junit = "0.5" +rustix = { version = "1", default-features = false, features = ["process"] } +tempfile = "3" +xshell = "0.2" diff --git a/tests/bootc-integration/bootc-e2e.md b/tests/bootc-integration/bootc-e2e.md new file mode 100644 index 00000000..9d309ac2 --- /dev/null +++ b/tests/bootc-integration/bootc-e2e.md @@ -0,0 +1,25 @@ +# Bootc integration tests + +Integration tests that run inside a [bcvk](https://github.com/bootc-dev/bcvk) VM +against a deployed ostree system. The test binary is baked into the container +image at build time and executed over SSH. + +Tests are split into two tiers: `booted_test!` tests need a fully deployed +system (via `bcvk libvirt run`) while `privileged_test!` tests just need root +and can run in a faster ephemeral VM. See the module docs in +`src/tests/privileged.rs` for details. + +## Running + +```sh +just build # build the container image +just integration-container # run all tests in a deployed VM +just integration-ephemeral # fast path: privileged-only tests +just integration-container --exact privileged_verify_composefs # single test +just integration-container --list # list tests +just integration-cleanup # remove leftover VMs +``` + +Set `JUNIT_OUTPUT` to a file path to get JUnit XML results. The +`integration-container` recipe does this automatically, writing to +`target/integration-results.xml`. diff --git a/tests/bootc-integration/src/lib.rs b/tests/bootc-integration/src/lib.rs new file mode 100644 index 00000000..37f2e0a1 --- /dev/null +++ b/tests/bootc-integration/src/lib.rs @@ -0,0 +1,52 @@ +//! Integration test infrastructure for ostree bootc testing. +//! +//! Provides test registration via [`linkme`] distributed slices and the +//! [`integration_test!`] macro, collected and executed by a custom +//! [`libtest_mimic`] harness in `main.rs`. + +// linkme requires unsafe for distributed slices +#![allow(unsafe_code)] + +/// A test function that returns a Result. +pub type TestFn = fn() -> anyhow::Result<()>; + +/// Metadata for a registered integration test. +#[derive(Debug)] +pub struct IntegrationTest { + /// Name of the integration test. + pub name: &'static str, + /// Test function to execute. + pub f: TestFn, +} + +impl IntegrationTest { + /// Create a new integration test with the given name and function. + pub const fn new(name: &'static str, f: TestFn) -> Self { + Self { name, f } + } +} + +/// Distributed slice holding all registered integration tests. +#[linkme::distributed_slice] +pub static INTEGRATION_TESTS: [IntegrationTest]; + +/// Register an integration test function. +/// +/// # Examples +/// +/// ```ignore +/// fn test_something() -> anyhow::Result<()> { +/// Ok(()) +/// } +/// integration_test!(test_something); +/// ``` +#[macro_export] +macro_rules! integration_test { + ($fn_name:ident) => { + ::paste::paste! { + #[::linkme::distributed_slice($crate::INTEGRATION_TESTS)] + static [<$fn_name:upper>]: $crate::IntegrationTest = + $crate::IntegrationTest::new(stringify!($fn_name), $fn_name); + } + }; +} diff --git a/tests/bootc-integration/src/main.rs b/tests/bootc-integration/src/main.rs new file mode 100644 index 00000000..1b893bfe --- /dev/null +++ b/tests/bootc-integration/src/main.rs @@ -0,0 +1,93 @@ +//! Integration test runner for ostree bootc testing. +//! +//! This binary uses [`libtest_mimic`] as a custom test harness. +//! Tests are registered via the [`integration_test!`] macro in submodules +//! and collected from the [`INTEGRATION_TESTS`] distributed slice at startup. +//! +//! When the `JUNIT_OUTPUT` environment variable is set to a file path, +//! JUnit XML results are written there after tests complete. + +// linkme requires unsafe for distributed slices +#![allow(unsafe_code)] + +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +use libtest_mimic::{Arguments, Trial}; + +pub(crate) use ostree_bootc_integration_tests::{integration_test, INTEGRATION_TESTS}; + +mod tests; + +/// Outcome of a single test, recorded during execution. +struct TestOutcome { + name: String, + duration: std::time::Duration, + result: Result<(), String>, +} + +fn main() { + let args = Arguments::from_args(); + let outcomes: Arc>> = Arc::new(Mutex::new(Vec::new())); + + let tests: Vec = INTEGRATION_TESTS + .iter() + .map(|t| { + let f = t.f; + let name = t.name.to_owned(); + let outcomes = Arc::clone(&outcomes); + Trial::test(t.name, move || { + let start = Instant::now(); + let result = f(); + let duration = start.elapsed(); + let outcome = TestOutcome { + name, + duration, + result: result.as_ref().map(|_| ()).map_err(|e| format!("{e:?}")), + }; + outcomes.lock().unwrap().push(outcome); + result.map_err(|e| format!("{e:?}").into()) + }) + }) + .collect(); + + let conclusion = libtest_mimic::run(&args, tests); + + // Write JUnit XML if requested + if let Ok(path) = std::env::var("JUNIT_OUTPUT") { + if let Err(e) = write_junit(&path, &outcomes.lock().unwrap()) { + eprintln!("warning: failed to write JUnit XML to {path}: {e}"); + } + } + + std::process::exit(if conclusion.has_failed() { 101 } else { 0 }); +} + +fn write_junit(path: &str, outcomes: &[TestOutcome]) -> anyhow::Result<()> { + use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite}; + + let mut report = Report::new("ostree-bootc-integration-tests"); + let mut suite = TestSuite::new("privileged"); + + for outcome in outcomes { + let status = match &outcome.result { + Ok(()) => TestCaseStatus::success(), + Err(msg) => { + let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure); + status.set_message(msg.clone()); + status + } + }; + let mut tc = TestCase::new(outcome.name.clone(), status); + tc.set_time(outcome.duration); + suite.add_test_case(tc); + } + + report.add_test_suite(suite); + let xml = report + .to_string() + .map_err(|e| anyhow::anyhow!("JUnit serialization failed: {e}"))?; + std::fs::write(path, xml)?; + eprintln!("JUnit XML written to {path}"); + Ok(()) +} diff --git a/tests/bootc-integration/src/tests/mod.rs b/tests/bootc-integration/src/tests/mod.rs new file mode 100644 index 00000000..1fd840e1 --- /dev/null +++ b/tests/bootc-integration/src/tests/mod.rs @@ -0,0 +1,3 @@ +//! Integration test modules. + +pub mod privileged; diff --git a/tests/bootc-integration/src/tests/privileged.rs b/tests/bootc-integration/src/tests/privileged.rs new file mode 100644 index 00000000..cfb87083 --- /dev/null +++ b/tests/bootc-integration/src/tests/privileged.rs @@ -0,0 +1,338 @@ +//! Integration tests requiring root privileges. +//! +//! Tests are split into two categories: +//! +//! * **Booted** (`booted_test!`) — need a fully deployed ostree system +//! (composefs, sysroot, boot markers). When not root these dispatch via +//! `bcvk libvirt run` which does a full `bootc install to-disk`. +//! +//! * **Privileged** (`privileged_test!`) — just need root and the ostree +//! binary installed. When not root these dispatch via the much faster +//! `bcvk ephemeral run-ssh` (no disk install). + +use anyhow::{ensure, Result}; +use xshell::{cmd, Shell}; + +use crate::integration_test; + +/// How a test should be dispatched when not running as root. +/// See the module-level docs for details on each mode. +enum RunMode { + Booted, + Privileged, +} + +macro_rules! booted_test { + ($fn_name:ident, $body:expr) => { + fn $fn_name() -> Result<()> { + if require_root(stringify!($fn_name), RunMode::Booted)?.is_some() { + return Ok(()); + } + $body + } + integration_test!($fn_name); + }; +} + +macro_rules! privileged_test { + ($fn_name:ident, $body:expr) => { + fn $fn_name() -> Result<()> { + if require_root(stringify!($fn_name), RunMode::Privileged)?.is_some() { + return Ok(()); + } + $body + } + integration_test!($fn_name); + }; +} + +/// Returns `Ok(None)` if already root (test proceeds normally). +/// Otherwise dispatches to a VM per `mode` and returns `Ok(Some(()))`. +fn require_root(test_name: &str, mode: RunMode) -> Result> { + if rustix::process::getuid().is_root() { + return Ok(None); + } + + // We're on the host without root — delegate to a VM. + if std::env::var_os("OSTREE_IN_VM").is_some() { + anyhow::bail!("OSTREE_IN_VM is set but we're not root — VM setup is broken"); + } + + let image = std::env::var("OSTREE_TEST_IMAGE").map_err(|_| { + anyhow::anyhow!( + "not root and OSTREE_TEST_IMAGE not set; \ + run `just integration-container` to build and test" + ) + })?; + + let sh = Shell::new()?; + let bcvk = std::env::var("BCVK_PATH").unwrap_or_else(|_| "bcvk".into()); + + match mode { + RunMode::Booted => { + // Use a unique VM name per test to avoid collisions + let vm_name = format!("ostree-test-{}", test_name.replace('_', "-")); + + // Deploy a full VM (bootc install to-disk + boot) and wait for SSH + cmd!( + sh, + "{bcvk} libvirt run --name {vm_name} --replace --detach --ssh-wait {image}" + ) + .run()?; + + // Run the test inside the deployed VM + let result = cmd!( + sh, + "{bcvk} libvirt ssh {vm_name} -- ostree-bootc-integration-tests --exact {test_name}" + ) + .run(); + + // Always clean up the VM + let _ = cmd!(sh, "{bcvk} libvirt rm --stop --force {vm_name}").run(); + + // Propagate the test result + result?; + } + RunMode::Privileged => { + // Fast path: ephemeral container, no disk install needed + cmd!( + sh, + "{bcvk} ephemeral run-ssh {image} -- ostree-bootc-integration-tests --exact {test_name}" + ) + .run()?; + } + } + + Ok(Some(())) +} + +booted_test!(privileged_verify_ostree_booted, { + let sh = Shell::new()?; + + // /run/ostree-booted is the canonical marker + ensure!( + std::path::Path::new("/run/ostree-booted").exists(), + "/run/ostree-booted does not exist — system was not booted via ostree" + ); + + // ostree admin status should succeed and show a deployment + let status = cmd!(sh, "ostree admin status").read()?; + ensure!( + !status.is_empty(), + "ostree admin status returned empty output" + ); + + Ok(()) +}); + +booted_test!(privileged_verify_sysroot, { + ensure!( + std::path::Path::new("/sysroot").is_dir(), + "/sysroot does not exist" + ); + ensure!( + std::path::Path::new("/ostree").is_dir(), + "/ostree does not exist" + ); + ensure!( + std::path::Path::new("/ostree/repo").is_dir(), + "/ostree/repo does not exist" + ); + + // Verify there's at least one deployment + let deploy_dir = std::path::Path::new("/ostree/deploy"); + ensure!(deploy_dir.is_dir(), "/ostree/deploy does not exist"); + + let has_stateroot = std::fs::read_dir(deploy_dir)? + .any(|entry| entry.is_ok_and(|e| e.file_type().is_ok_and(|ft| ft.is_dir()))); + ensure!( + has_stateroot, + "no stateroot directories found under /ostree/deploy" + ); + + Ok(()) +}); + +booted_test!(privileged_verify_composefs, { + let sh = Shell::new()?; + + // With composefs enabled, the root filesystem should be an overlay + let fstype = cmd!(sh, "findmnt -n -o FSTYPE /").read()?; + ensure!( + fstype.trim() == "overlay", + "expected root filesystem type 'overlay' (composefs), got '{}'", + fstype.trim() + ); + + // The composefs private directory should exist with restricted permissions + let private_dir = std::path::Path::new("/run/ostree/.private"); + ensure!( + private_dir.exists(), + "/run/ostree/.private does not exist — composefs may not be active" + ); + + let metadata = std::fs::metadata(private_dir)?; + let mode = std::os::unix::fs::PermissionsExt::mode(&metadata.permissions()) & 0o777; + ensure!( + mode == 0, + "/run/ostree/.private has mode {mode:#o}, expected 0o000" + ); + + Ok(()) +}); + +booted_test!(privileged_verify_ostree_cli, { + let sh = Shell::new()?; + + // Verify ostree binary works and reports a version + let version_output = cmd!(sh, "ostree --version").read()?; + ensure!( + version_output.contains("libostree"), + "ostree --version output doesn't mention libostree: {version_output}" + ); + + // ostree admin status should succeed + let status = cmd!(sh, "ostree admin status").read()?; + ensure!( + !status.is_empty(), + "ostree admin status returned empty output" + ); + + // ostree refs should succeed (may be empty, but shouldn't error) + cmd!(sh, "ostree refs --repo=/ostree/repo").run()?; + + Ok(()) +}); + +booted_test!(privileged_verify_sysroot_readonly, { + let sh = Shell::new()?; + let options = cmd!(sh, "findmnt -n -o OPTIONS /sysroot").read()?; + ensure!( + options.contains("ro"), + "/sysroot is not mounted read-only: {options}" + ); + Ok(()) +}); + +booted_test!(privileged_verify_ostree_run_metadata, { + let metadata = std::fs::metadata("/run/ostree")?; + let mode = std::os::unix::fs::PermissionsExt::mode(&metadata.permissions()) & 0o777; + ensure!( + mode == 0o755, + "/run/ostree has mode {mode:#o}, expected 0o755" + ); + Ok(()) +}); + +booted_test!(privileged_verify_immutable_bit, { + let sh = Shell::new()?; + // If composefs (overlay), the immutable bit doesn't apply + let fstype = cmd!(sh, "findmnt -n -o FSTYPE /").read()?; + if fstype.trim() == "overlay" { + return Ok(()); + } + // https://bugzilla.redhat.com/show_bug.cgi?id=1867601 + let lsattr_out = cmd!(sh, "lsattr -d /").read()?; + ensure!( + lsattr_out.contains("-i-"), + "immutable bit not set on /: {lsattr_out}" + ); + Ok(()) +}); + +booted_test!(privileged_verify_osinit_unshare, { + let sh = Shell::new()?; + // Create a test stateroot; this should not disturb /run/ostree permissions + cmd!(sh, "ostree admin os-init ostreetestsuite").run()?; + let metadata = std::fs::metadata("/run/ostree")?; + let mode = std::os::unix::fs::PermissionsExt::mode(&metadata.permissions()) & 0o777; + ensure!( + mode == 0o755, + "/run/ostree has mode {mode:#o} after os-init, expected 0o755" + ); + Ok(()) +}); + +privileged_test!(privileged_verify_nofifo, { + let sh = Shell::new()?; + let tmp = tempfile::tempdir()?; + let tmpdir = tmp.path(); + let _guard = sh.push_dir(tmpdir); + cmd!(sh, "ostree --repo=repo init --mode=archive").run()?; + cmd!(sh, "mkdir tmproot").run()?; + cmd!(sh, "mkfifo tmproot/afile").run()?; + let result = cmd!( + sh, + "ostree --repo=repo commit -b fifotest -s 'commit fifo' --tree=dir=./tmproot" + ) + .ignore_status() + .output()?; + ensure!( + !result.status.success(), + "committing a FIFO should have failed" + ); + let stderr = String::from_utf8_lossy(&result.stderr); + ensure!( + stderr.contains("Not a regular file or symlink"), + "expected 'Not a regular file or symlink' in stderr, got: {stderr}" + ); + Ok(()) +}); + +privileged_test!(privileged_verify_mtime, { + let sh = Shell::new()?; + let tmp = tempfile::tempdir()?; + let tmpdir = tmp.path(); + let _guard = sh.push_dir(tmpdir); + cmd!(sh, "ostree --repo=repo init --mode=archive").run()?; + cmd!(sh, "mkdir tmproot").run()?; + cmd!(sh, "bash -c 'echo afile > tmproot/afile'").run()?; + cmd!(sh, "ostree --repo=repo commit -b test --tree=dir=tmproot") + .ignore_stdout() + .run()?; + let ts_before = tmpdir.join("repo").metadata()?.modified()?; + std::thread::sleep(std::time::Duration::from_secs(1)); + cmd!( + sh, + "ostree --repo=repo commit -b test -s 'bump mtime' --tree=dir=tmproot" + ) + .ignore_stdout() + .run()?; + let ts_after = tmpdir.join("repo").metadata()?.modified()?; + ensure!( + ts_before != ts_after, + "repo mtime did not change after second commit" + ); + Ok(()) +}); + +privileged_test!(privileged_verify_extensions, { + let sh = Shell::new()?; + let tmp = tempfile::tempdir()?; + let tmpdir = tmp.path(); + let _guard = sh.push_dir(tmpdir); + cmd!(sh, "ostree --repo=repo init --mode=bare").run()?; + let extensions = tmpdir.join("repo/extensions"); + ensure!( + extensions.exists(), + "repo/extensions directory was not created by ostree init" + ); + Ok(()) +}); + +booted_test!(privileged_verify_selinux_labels, { + let sh = Shell::new()?; + + // Check that /etc has the correct SELinux type + let ls_output = cmd!(sh, "ls -dZ /etc").read()?; + if !ls_output.contains(':') { + // SELinux is likely disabled; skip this check + return Ok(()); + } + ensure!( + ls_output.contains(":etc_t:"), + "/etc does not have :etc_t: SELinux label: {ls_output}" + ); + + Ok(()) +}); -- 2.39.5